package net;

// TCP回显服务器

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class TcpEchoServer {
    // 代码中会涉及到多个socket对象，使用不同的名字来进行区分
    private ServerSocket listenSocket = null;
    // listenSocket 类似于外场销售，负责拉人头

    // 构造方法（传入端口号进行绑定）
    public TcpEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    // start方法
    public void start() throws IOException {
        System.out.println("服务器启动！");

        // 创建线程池：注意创建方法以及关键字！
        ExecutorService service = Executors.newCachedThreadPool();

        while (true) {
            // 完成一个请求
            // 1. 先调用accept来接受客户端的连接
            Socket clientSocket = listenSocket.accept();
            // clientSocket类似于置业顾问，负责和具体的客户对接
            // 所以：一个listenSocket会对应多个clientSocket
            // 如果没有客户端来建立连接，则accept会阻塞

            // 2. 再处理这个连接
            // 此时如果多个客户端的话就会在hasNext阻塞，导致其他客户端无法被响应，所以这里使用多线程处理
            // 所以：使用多线程进行处理，每个客户端分配一个线程
            // 此处使用线程会造成线程的频繁创建和销毁
            // 则改进方法：线程池
            /*Thread t = new Thread(()-> {
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            // 不要忘记调用start开始线程！
            t.start();*/

            // 使用线程池进行改进
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    // 处理来自客户端的连接
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s : %d]客户端上线!（建立了连接）\n",
                clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // 接下来就处理客户端的请求（拿到输入流,输出流）
        // clientSocket 就是在从网卡/客户端读写数据
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            while (true) {
                // 循环是因为：客户端可能会发多个请求

                // 1. 读取请求并解析（注意Scanner读）
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()) {
                    // 说明此时已经读完了，断开连接
                    System.out.printf("[%s : %d] 客户端下线！\n",
                            clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                // 读取请求
                String request = scanner.next();

                // 2. 根据请求计算响应
                String response = process(request);

                // 3. 把响应写回给客户端(注意使用PrintWriter写）
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response); // 响应写回客户端
                // 刷新缓冲区，确保数据确实是通过网卡发送出去了！！
                printWriter.flush();

                // 4.打印日志
                System.out.printf("client[%s:%d] req:%s; resp:%s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭clientSocket
            // 为啥此处要关闭socket，而前面的listenSocket以及udp程序中的socket为啥没关闭？
            // 理由：socket也是文件，而一个进程能够打开的文件个数是有上限的（因为PCB文件描述符表不是无限的）。
            // listenSocket在TCP服务器程序中只有唯一的对象，就不太会把文件描述符表占满；
            // 而clientSocket则是在循环中，也就是说每个客户端进来都要分配一个，则该对象就会被反复创建出实例，
            // 每创建一个实例对象都要消耗一个文件描述符表，因此就需要把不再使用的clientSocket释放掉。

            clientSocket.close();
        }
    }

    // 根据请求计算响应
    public String process(String request) {
        return request;
    }
    // 改为public修饰，方便后续进行继承时重写该方法


    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);  // 任意给服务器绑定一个端口号（一般是1024~65535）
        server.start();
    }
}


// 上述一次处理多个请求是长连接，要想实现短连接（一次一个请求）的话就删去循环（clientSocket以及输入处的循环），
// 保证一次一个服务器处理一个请求